package org.test4j.mock.stub;

import g_cglib.net.sf.cglib.core.CodeGenerationException;
import g_cglib.net.sf.cglib.core.DefaultNamingPolicy;
import g_cglib.net.sf.cglib.core.NamingPolicy;
import g_cglib.net.sf.cglib.core.Predicate;
import g_cglib.net.sf.cglib.proxy.*;
import g_objenesis.org.objenesis.Objenesis;
import g_objenesis.org.objenesis.ObjenesisStd;
import org.test4j.mock.faking.util.ClassLoad;
import org.test4j.mock.faking.util.TypeUtility;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static java.lang.reflect.Modifier.isAbstract;

/**
 * 欺骗者, 使用Enhancer来创建Stub或Proxy对象
 *
 * @author darui.wu
 */
public class Impostor {
    private Impostor() {
    }

    private static final NamingPolicy NAMING_POLICY = new DefaultNamingPolicy() {
        @Override
        public String getClassName(String prefix, String source, Object key, Predicate names) {
            return "org.test4j.stub." + super.getClassName(prefix, source, key, names);
        }
    };

    private static final Objenesis objenesis = new ObjenesisStd();

    /**
     * 生成stub class类
     *
     * @param isStub     true: stub object; false: proxy object
     * @param baseType
     * @param interfaces
     * @return
     */
    static Class createStubClass(boolean isStub, Class baseType, Class... interfaces) {
        if (baseType == Object.class) {
            baseType = ObjectStub.class;
        }
        Enhancer enhancer = new Enhancer() {
            @Override
            protected void filterConstructors(Class sc, List constructors) {
                // Don't filter
            }
        };
        enhancer.setUseCache(false);
        enhancer.setClassLoader(ClassLoad.loadersOf(baseType, interfaces));
        enhancer.setUseFactory(true);
        if (baseType.isInterface()) {
            enhancer.setSuperclass(ObjectStub.class);
            enhancer.setInterfaces(toArray(baseType, interfaces));
        } else {
            enhancer.setSuperclass(baseType);
            enhancer.setInterfaces(interfaces);
        }
        if (isStub) {
            enhancer.setCallbackTypes(new Class[]{MethodInterceptor.class, NoOp.class});
            enhancer.setCallbackFilter(method -> isAbstract(method.getModifiers()) ? 0 : 1);
        } else {
            enhancer.setCallbackTypes(new Class[]{InvocationHandler.class, NoOp.class});
            enhancer.setCallbackFilter(method -> method.isBridge() ? 1 : 0);
        }
        if (baseType.getSigners() != null) {
            enhancer.setNamingPolicy(NAMING_POLICY);
        }
        try {
            return enhancer.createClass();
        } catch (CodeGenerationException e) {
            throw new IllegalArgumentException("could not proxy " + baseType, e);
        }
    }

    /**
     * 创建proxy对象
     *
     * @param invokable
     * @param baseType
     * @param interfaces
     * @return
     */
    public static <T> T proxy(final ProxyInvokable invokable, Class baseType, Class... interfaces) {
        return enhancer(
            (InvocationHandler) (receiver, method, args) -> invokable.invoke(new ProxyInvocation(receiver, method, args)),
            null, baseType, interfaces);
    }

    /**
     * 创建fake代理桩
     *
     * @param baseType
     * @param args
     * @param <T>
     * @return
     */
    public static <T> T fake(Class baseType, Object... args) {
        return stub(new FakeInterceptor(baseType), args, baseType, IFakeStub.class);
    }

    /**
     * 创建stub对象
     *
     * @param interceptor
     * @param baseType
     * @param interfaces
     * @param <T>
     * @return
     */
    public static <T> T stub(final MethodInterceptor interceptor, Object[] args, Class baseType, Class... interfaces) {
        return enhancer(interceptor, args == null ? new Object[0] : args, baseType, interfaces);
    }

    static <T> T enhancer(final Callback callback, Object[] args, Class baseType, Class... interfaces) {
        if (!canProxy(baseType)) {
            throw new IllegalArgumentException(baseType.getName() + " can't be proxied.");
        }
        setConstructorsAccessible(baseType, true);
        Class proxyClass = createStubClass(args != null, baseType, interfaces);
        Factory factory;
        if (args == null || args.length == 0 || baseType.isInterface()) {
            factory = (Factory) objenesis.newInstance(proxyClass);
        } else {
            factory = newInstance(proxyClass, baseType, args);
        }
        factory.setCallbacks(new Callback[]{callback, NoOp.INSTANCE});
        return (T) baseType.cast(factory);
    }

    private static Factory newInstance(Class proxyClass, Class baseType, Object[] args) {
        Constructor[] constructors = proxyClass.getDeclaredConstructors();
        Constructor constructor = findMatchConstructor(constructors, args);
        if (constructor == null) {
            throw new RuntimeException("not found constructor for args:" + describe(args));
        }
        try {
            return (Factory) constructor.newInstance(args);
        } catch (Exception e) {
            throw new RuntimeException("New instance[" + baseType.getName() + "] for args:" + describe(args) + " error.", e);
        }
    }

    private static Class[] toArray(Class first, Class... rest) {
        Class[] all = new Class[rest.length + 1];
        all[0] = first;
        System.arraycopy(rest, 0, all, 1, rest.length);
        return all;
    }

    private static void setConstructorsAccessible(Class mockedType, boolean accessible) {
        for (Constructor<?> constructor : mockedType.getDeclaredConstructors()) {
            constructor.setAccessible(accessible);
        }
    }

    public static boolean canProxy(Class type) {
        return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers());
    }

    /**
     * 返回符合args类型的构造函数参数类型列表
     *
     * @param constructors
     * @param args
     * @return
     */
    public static Constructor findMatchConstructor(Constructor[] constructors, Object[] args) {
        for (Constructor constructor : constructors) {
            int count = constructor.getParameterCount();
            if (count != args.length) {
                continue;
            }
            Class[] types = constructor.getParameterTypes();
            Constructor matched = constructor;
            for (int index = 0; index < count; index++) {
                if (args[index] == null) {
                    continue;
                }
                Class pType = types[index];
                Class aType = args[index].getClass();
                if (TypeUtility.isAssignable(pType, aType)) {
                    continue;
                }
                matched = null;
                break;
            }
            if (matched != null) {
                return matched;
            }
        }
        return null;
    }

    public static String describe(Object[] args) {
        if (args == null) {
            return null;
        }
        return Arrays.stream(args).map(String::valueOf).collect(Collectors.joining(",", "(", ")"));
    }
}